/*
    libhdvbd - Library for building sophisticated DVB applications
    Copyright (C) 2012 Tony Houghton <h@realh.co.uk>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include <glib.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
#include <unistd.h>
#include <linux/dvb/dmx.h>
#include <sys/ioctl.h>

#include "i18n.h"
#include "hdvb-receiver.h"

guint hdvb_receiver_signals[5];

/* Implementation functions */

inline static void
inline_lock(HdvbReceiver *rcv)
{
    g_rec_mutex_lock(&rcv->mutex);
}

inline static void
inline_unlock(HdvbReceiver *rcv)
{
    g_rec_mutex_unlock(&rcv->mutex);
}


typedef struct {
    int fd;
    int pid;
    int ref_count;
} HdvbReceiverFilter;


static void
cancel_timeout(HdvbReceiver *rcv)
{
    if (rcv->timeout_tag)
    {
        g_source_remove(rcv->timeout_tag);
        rcv->timeout_tag = 0;
    }
}

static void
cancel_poll_for_lock(HdvbReceiver *rcv)
{
    if (rcv->lock_tag)
    {
        g_source_remove(rcv->lock_tag);
        rcv->lock_tag = 0;
    }
    cancel_timeout(rcv);
}

static void
free_lock_ioc(HdvbReceiver *rcv)
{
    if (rcv->lock_ioc)
    {
        cancel_poll_for_lock(rcv);
        /* As the channel is only used for polling, no need to shutdown */
        g_io_channel_unref(rcv->lock_ioc);
        rcv->lock_ioc = NULL;
    }
}

static int
open_frontend(HdvbReceiver *rcv)
{
    inline_lock(rcv);
    if (rcv->fe_fd == -1)
        rcv->fe_fd = open(rcv->fe_name, O_RDWR);
    inline_unlock(rcv);
    return rcv->fe_fd;
}

static int
open_dvr(HdvbReceiver *rcv)
{
    inline_lock(rcv);
    if (rcv->dvr_fd == -1)
    {
        rcv->dvr_fd = open(rcv->dvr_name, O_RDONLY);
    }
    inline_unlock(rcv);
    return rcv->dvr_fd;
}

static void
rcv_delete_stream_src(HdvbReceiver *rcv)
{
    if (rcv->stream_src)
    {
        if (rcv->stream_src_deleter)
            rcv->stream_src_deleter(rcv->stream_src);
        rcv->stream_src = NULL;
    }
}

static void
close_dvr(HdvbReceiver *rcv)
{
    inline_lock(rcv);
    if (rcv->dvr_fd >= 0)
    {
        int fd = rcv->dvr_fd;

        rcv->dvr_fd = -1;
        close(fd);
    }
    inline_unlock(rcv);
}

static void
close_frontend_full(HdvbReceiver *rcv, gboolean emit_detune)
{
    inline_lock(rcv);
    free_lock_ioc(rcv);
    if (emit_detune)
    {
        g_signal_emit(rcv, hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_DETUNE],
                0, NULL);
    }
    g_free(rcv->tuning_str);
    rcv->tuning_str = NULL;
    close_dvr(rcv);
    if (rcv->fe_fd != -1)
    {
        close(rcv->fe_fd);
        rcv->fe_fd = -1;
    }
    inline_unlock(rcv);
}

static int
tune(HdvbReceiver *rcv, const HdvbTuningData *td, const char *tuning_str,
        guint timeout)
{
    int fd;
    struct dtv_property clear_prop;
    struct dtv_properties fe_props;

    inline_lock(rcv);
    if (hdvb_receiver_is_tuned_to(rcv, tuning_str))
    {
        g_debug(_("%d:%d Already tuned to %s"), rcv->adapter, rcv->frontend,
                tuning_str);
        inline_unlock(rcv);
        return 0;
    }
    g_signal_emit(rcv, hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_DETUNE], 0,
            tuning_str);
    g_free(rcv->tuning_str);
    rcv->tuning_str = NULL;
    hdvb_receiver_close_dvr(rcv);
    g_message(_("Tuning %d:%d to '%s'"),
            rcv->adapter, rcv->frontend, tuning_str);
    fd = hdvb_receiver_open_frontend(rcv);
    fe_props.num = 1;
    fe_props.props = &clear_prop;
    clear_prop.cmd = DTV_CLEAR;
    clear_prop.u.data = 0;
    if (fd != -1 && ioctl(fd, FE_SET_PROPERTY, &fe_props) < 0)
    {
        hdvb_receiver_close_frontend_full(rcv, FALSE);
        fd = -1;
    }
    if (td->type == FE_QPSK)
    {
        if (fd != -1 && ioctl(fd, FE_SET_TONE, td->tone) < 0)
        {
            hdvb_receiver_close_frontend_full(rcv, FALSE);
            fd = -1;
        }
    }
    if (fd != -1 && ioctl(fd, FE_SET_PROPERTY, &td->pprops) < 0)
    {
        hdvb_receiver_close_frontend_full(rcv, FALSE);
        fd = -1;
    }
    rcv->tuning_str = (fd == -1) ? NULL : g_strdup(tuning_str);
    inline_unlock(rcv);
    if (fd != -1 && timeout)
        hdvb_receiver_poll_for_lock(rcv, timeout);
    return fd != -1 ? 0 : errno;
}

static int
request_ts_filter(HdvbReceiver *rcv, int pid, dmx_pes_type_t pes_type)
{
    GList *link;
    HdvbReceiverFilter *filt;
    struct dmx_pes_filter_params params;

    inline_lock(rcv);
    for (link = rcv->ts_filters; link; link = g_list_next(link))
    {
        filt = link->data;
        if (filt->pid == pid)
        {
            ++filt->ref_count;
            inline_unlock(rcv);
            return 0;
        }
    }
    filt = g_slice_new(HdvbReceiverFilter);
    filt->fd = open(rcv->dmx_name, O_RDWR | O_NONBLOCK);
    if (filt->fd < 0)
    {
        g_slice_free(HdvbReceiverFilter, filt);
        inline_unlock(rcv);
        return errno;
    }
    params.pid = pid;
    params.input = DMX_IN_FRONTEND;
    params.output = DMX_OUT_TS_TAP;
    params.pes_type = DMX_PES_OTHER;
    params.flags = DMX_IMMEDIATE_START;
    if (ioctl(filt->fd, DMX_SET_PES_FILTER, &params) < 0)
    {
        int result = errno;

        close(filt->fd);
        g_slice_free(HdvbReceiverFilter, filt);
        inline_unlock(rcv);
        return result;
    }
    filt->pid = pid;
    filt->ref_count = 1;
    rcv->ts_filters = g_list_prepend(rcv->ts_filters, filt);
    inline_unlock(rcv);
    return 0;
}

static void
unref_ts_filter(HdvbReceiver *rcv, int pid)
{
    GList *link;

    inline_lock(rcv);
    for (link = rcv->ts_filters; link; link = g_list_next(link))
    {
        HdvbReceiverFilter *filt = link->data;

        if (filt->pid == pid)
        {
            if (!--filt->ref_count)
            {
                ioctl(filt->fd, DMX_STOP, NULL);
                close(filt->fd);
                rcv->ts_filters = g_list_delete_link(rcv->ts_filters, link);
                g_slice_free(HdvbReceiverFilter, filt);
            }
        }
    }
    inline_unlock(rcv);
}

static gboolean
lock_dispatch(HdvbReceiver *rcv, gboolean locked)
{
    cancel_poll_for_lock(rcv);
    if (!locked)
    {
        hdvb_receiver_close_frontend_full(rcv, FALSE);
    }
    else
    {
        g_message("Receiver %d:%d locked", rcv->adapter, rcv->frontend);
    }
    g_signal_emit(rcv, hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_LOCK],
            0, locked);
    inline_unlock(rcv);
    return FALSE;
}

static gboolean
locked_cb(GIOChannel *ioc, GIOCondition condition, HdvbReceiver *rcv)
{
    struct dvb_frontend_event event;
    gboolean locked = FALSE;

    inline_lock(rcv);
    if (condition & (G_IO_IN | G_IO_PRI))
    {
        if (ioctl(rcv->fe_fd, FE_GET_EVENT, &event))
        {
            if (errno == EOVERFLOW)
            {
                return TRUE;
            }
            else
            {
                g_warning(_("%d:%d lock check: ioctl failed: %s"),
                        rcv->adapter, rcv->frontend, strerror(errno));
            }
        }
        else
        {
            locked = (event.status & FE_HAS_LOCK) != 0;
            if (!locked)
            {
                inline_unlock(rcv);
                return TRUE;
            }
        }
    }
    else
    {
        g_warning(_("Error raised while waiting for %d:%d to lock"),
                rcv->adapter, rcv->frontend);
    }
    return lock_dispatch(rcv, locked);
}

static gboolean
lock_timeout_cb(gpointer handle)
{
    HdvbReceiver *rcv = handle;

    inline_lock(rcv);
    g_warning(_("%d:%d signal lock on '%s' timed out"),
            rcv->adapter, rcv->frontend, rcv->tuning_str);
    return lock_dispatch(rcv, FALSE);
}

static void
poll_for_lock(HdvbReceiver *rcv, guint timeout)
{
    cancel_poll_for_lock(rcv);
    if (!rcv->lock_ioc)
    {
        rcv->lock_ioc = g_io_channel_unix_new(hdvb_receiver_open_frontend(rcv));
    }
    rcv->lock_tag = g_io_add_watch(rcv->lock_ioc,
            G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_NVAL,
            (GIOFunc) locked_cb, rcv);
    rcv->timeout_tag = g_timeout_add(timeout, lock_timeout_cb, rcv);
}

static int
set_section_filter(HdvbReceiver *rcv,
        const struct dmx_sct_filter_params *params)
{
    int fd = open(rcv->dmx_name, O_RDONLY | O_NONBLOCK);

    if (fd < 0)
        return -errno;
    if (ioctl(fd, DMX_SET_FILTER, params))
    {
        int result = -errno;
        close(fd);
        return result;
    }
    return fd;
}

static void
hdvb_receiver_set_names(HdvbReceiver *rcv)
{
    g_free(rcv->fe_name);
    g_free(rcv->dmx_name);
    g_free(rcv->dvr_name);
    rcv->fe_name = g_strdup_printf("/dev/dvb/adapter%d/frontend%d",
            rcv->adapter, rcv->frontend);
    rcv->dmx_name = g_strdup_printf("/dev/dvb/adapter%d/demux%d",
            rcv->adapter, rcv->frontend);
    rcv->dvr_name = g_strdup_printf("/dev/dvb/adapter%d/dvr%d",
            rcv->adapter, rcv->frontend);
}

static gboolean
hdvb_receiver_numbers_from_devname(const char *devname,
        int *adapter, int *frontend)
{
    gboolean result;
    const char *adstr = strstr(devname, "adapter");

    if (adstr)
    {
        result = (sscanf(adstr, "adapter%d/frontend%d",
                adapter, frontend) == 2);
    }
    else
    {
        result = FALSE;
    }
    if (!result)
    {
        g_warning(_("Unable to map device '%s' to a DVB receiver object"),
                devname);
        *adapter = 0;
        *frontend = 0;
    }
    return result;
}

inline static gboolean
hdvb_receiver_numbers_from_udev(GUdevDevice *udev, int *adapter, int *frontend)
{
    return hdvb_receiver_numbers_from_devname(
            g_udev_device_get_device_file(udev), adapter, frontend);
}

static void
reserve(HdvbReceiver *rcv)
{
    g_signal_emit(rcv, hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_RESERVE], 0);
}

static void
event_status(HdvbReceiver *rcv, gboolean running)
{
    g_signal_emit(rcv, hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_EVENT_STATUS],
            0, running);
}

static HdvbStreamSrcPtr
get_stream_src(HdvbReceiver *rcv)
{
    inline_lock(rcv);
    if (!rcv->stream_src)
    {
        if (rcv->stream_src_factory)
        {
            rcv->stream_src = rcv->stream_src_factory(rcv, rcv->factory_handle);
        }
        else
        {
            g_critical(_("Receiver %d:%d has no stream src factory"),
                    rcv->adapter, rcv->frontend);
        }
    }
    inline_unlock(rcv);
    return rcv->stream_src;
}

static void
set_stream_src_factory(HdvbReceiver *rcv,
        HdvbStreamSrcFactory factory, HdvbStreamSrcDeleter deleter,
        gpointer handle)
{
    inline_lock(rcv);
    rcv_delete_stream_src(rcv);
    rcv->stream_src_factory = factory;
    rcv->stream_src_deleter = deleter;
    if (!deleter)
        g_warning(_("Stream src deleter should not be NULL"));
    rcv->factory_handle = handle;
    inline_unlock(rcv);
}

static gboolean
read_tuning_details(HdvbReceiver *rcv, HdvbTuningData *td)
{
    int fd;
    int n = 0;
    gboolean result = TRUE;

    inline_lock(rcv);
    fd = hdvb_receiver_open_frontend(rcv);
    if (fd < 0)
        result = FALSE;

    if (result)
    {
        td->type = hdvb_receiver_get_fe_type(rcv);
        td->pprops.props = td->props;
        td->props[n++].cmd = DTV_DELIVERY_SYSTEM;
        td->pprops.num = n;
        if (ioctl(fd, FE_GET_PROPERTY, &td->pprops) < 0)
        {
            g_warning(_("Error reading delivery system type from %d:%d: %s"),
                    rcv->adapter, rcv->frontend, strerror(errno));
            result = FALSE;
        }
    }

    if (result)
    {
        td->props[n++].cmd = DTV_FREQUENCY;
        switch (td->type)
        {
            case FE_OFDM:
                td->props[n++].cmd = DTV_BANDWIDTH_HZ;
                td->props[n++].cmd = DTV_CODE_RATE_HP;
                td->props[n++].cmd = DTV_CODE_RATE_LP;
                td->props[n++].cmd = DTV_MODULATION;
                td->props[n++].cmd = DTV_TRANSMISSION_MODE;
                td->props[n++].cmd = DTV_GUARD_INTERVAL;
                td->props[n++].cmd = DTV_HIERARCHY;
                if (td->props[0].u.data == SYS_DVBT2)
                    td->props[n++].cmd = DTV_STREAM_ID;
                break;
            case FE_QPSK:
                td->props[n++].cmd = DTV_VOLTAGE;
                td->props[n++].cmd = DTV_SYMBOL_RATE;
                td->props[n++].cmd = DTV_INNER_FEC;
                td->props[n++].cmd = DTV_MODULATION;
                if (td->props[0].u.data == SYS_DVBT2)
                {
                    td->props[n++].cmd = DTV_PILOT;
                    td->props[n++].cmd = DTV_ROLLOFF;
                }
                break;
            /* FIXME: Cable, ATSC etc */
            default:
                g_warning(_("Reading tuning details of frontend type %d "
                        "not supported"), td->type);
                result = FALSE;
                break;
        }
    }
    if (result)
    {
        td->pprops.num = n;
        if (ioctl(fd, FE_GET_PROPERTY, &td->pprops) < 0)
        {
            g_warning(_("Error reading tuning details from %d:%d: %s"),
                    rcv->adapter, rcv->frontend, strerror(errno));
            result = FALSE;
        }
    }

    inline_unlock(rcv);
    return result;
}

/* GObject boiler-plate */

G_DEFINE_TYPE(HdvbReceiver, hdvb_receiver, G_TYPE_OBJECT);

enum {
    PROP_0,

    PROP_ADAPTER,
    PROP_FRONTEND,
    PROP_UDEV,
};

static void
hdvb_receiver_set_property(GObject *object, guint property_id,
        const GValue *value, GParamSpec *pspec)
{
    HdvbReceiver *rcv = HDVB_RECEIVER(object);

    switch (property_id)
    {
        case PROP_UDEV:
            if (rcv->udev)
            {
                g_object_unref(rcv->udev);
            }
            rcv->udev = g_value_get_pointer(value);
            if (rcv->udev)
            {
                g_object_ref(rcv->udev);
                hdvb_receiver_numbers_from_udev(rcv->udev,
                        &rcv->adapter, &rcv->frontend);
                hdvb_receiver_set_names(rcv);
            }
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
            break;
    }
}

static void
hdvb_receiver_get_property(GObject *object, guint property_id,
        GValue *value, GParamSpec *pspec)
{
    HdvbReceiver *rcv = HDVB_RECEIVER(object);

    switch (property_id)
    {
        case PROP_ADAPTER:
            g_value_set_int(value, rcv->adapter);
            break;
        case PROP_FRONTEND:
            g_value_set_int(value, rcv->frontend);
            break;
        case PROP_UDEV:
            g_value_set_pointer(value, rcv->udev);
            break;
        default:
            G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
            break;
    }
}

static void
hdvb_receiver_init(HdvbReceiver *rcv)
{
    g_rec_mutex_init(&rcv->mutex);
    rcv->fe_fd = rcv->dvr_fd = -1;
    rcv->ts_filters = NULL;
    rcv->udev = NULL;
    rcv->adapter = 0;
    rcv->frontend = 0;
    rcv->fe_name = rcv->dmx_name = rcv->dvr_name = NULL;
    rcv->lock_tag = rcv->timeout_tag = 0;
    rcv->lock_ioc = NULL;
    rcv->tuning_str = NULL;
    rcv->start_pending = rcv->end_pending = FALSE;
    rcv->stream_src = NULL;
    rcv->stream_src_factory = NULL;
    rcv->stream_src_deleter = NULL;
}

static void
hdvb_receiver_dispose(GObject *gobj)
{
    HdvbReceiver *rcv = HDVB_RECEIVER(gobj);

    rcv_delete_stream_src(rcv);
    hdvb_receiver_close_dvr(rcv);
    hdvb_receiver_close_frontend(rcv);
    if (rcv->udev)
    {
        g_object_unref(rcv->udev);
        rcv->udev = NULL;
    }
    G_OBJECT_CLASS(hdvb_receiver_parent_class)->dispose(gobj);
}

static void
hdvb_receiver_finalize(GObject *gobj)
{
    HdvbReceiver *rcv = HDVB_RECEIVER(gobj);

    g_rec_mutex_clear(&rcv->mutex);
    g_free(rcv->fe_name);
    rcv->fe_name = NULL;
    g_free(rcv->dmx_name);
    rcv->dmx_name = NULL;
    g_free(rcv->dvr_name);
    rcv->dvr_name = NULL;
    G_OBJECT_CLASS(hdvb_receiver_parent_class)->finalize(gobj);
}

static void
hdvb_receiver_class_init(HdvbReceiverClass *klass)
{
    GObjectClass *gclass = G_OBJECT_CLASS(klass);
    GParamSpec *pspec;

    gclass->set_property = hdvb_receiver_set_property;
    gclass->get_property = hdvb_receiver_get_property;
    pspec = g_param_spec_int("adapter",
            _("Adapter number"), _("Set adapter number"),
            0, 255, 0,
            G_PARAM_READABLE);
    g_object_class_install_property(gclass,
            PROP_ADAPTER, pspec);
    pspec = g_param_spec_int("frontend",
            _("Frontend number"), _("Set frontend number"),
            0, 255, 0,
            G_PARAM_READABLE);
    g_object_class_install_property(gclass,
            PROP_FRONTEND, pspec);
    pspec = g_param_spec_pointer("udev",
            _("Udev device"), _("Pointer to a GUdevDevice"),
            G_PARAM_CONSTRUCT | G_PARAM_READWRITE);
    g_object_class_install_property(gclass,
            PROP_UDEV, pspec);

    gclass->dispose = hdvb_receiver_dispose;
    gclass->finalize = hdvb_receiver_finalize;

    klass->open_frontend = open_frontend;
    klass->close_frontend_full = close_frontend_full;
    klass->open_dvr = open_dvr;
    klass->close_dvr = close_dvr;
    klass->tune = tune;
    klass->request_ts_filter = request_ts_filter;
    klass->unref_ts_filter = unref_ts_filter;
    klass->poll_for_lock = poll_for_lock;
    klass->set_section_filter = set_section_filter;
    klass->reserve = reserve;
    klass->event_status = event_status;
    klass->get_stream_src = get_stream_src;
    klass->set_stream_src_factory = set_stream_src_factory;
    klass->read_tuning_details = read_tuning_details;

    hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_LOCK] =
            g_signal_new("lock", G_TYPE_FROM_CLASS(gclass),
                G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                0, NULL, NULL,
                g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE,
                1, G_TYPE_BOOLEAN);
    hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_DETUNE] =
            g_signal_new("detune", G_TYPE_FROM_CLASS(gclass),
                G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                0, NULL, NULL,
                g_cclosure_marshal_VOID__STRING, G_TYPE_NONE,
                1, G_TYPE_STRING);
    hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_RESERVE] =
            g_signal_newv("reserve", G_TYPE_FROM_CLASS(gclass),
                G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                NULL, NULL, NULL,
                g_cclosure_marshal_VOID__VOID, G_TYPE_NONE,
                0, NULL);
    hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_REMOVED] =
            g_signal_newv("removed", G_TYPE_FROM_CLASS(gclass),
                G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                NULL, NULL, NULL,
                g_cclosure_marshal_VOID__VOID, G_TYPE_NONE,
                0, NULL);
    hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_EVENT_STATUS] =
            g_signal_new("event-status", G_TYPE_FROM_CLASS(gclass),
                G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                0, NULL, NULL,
                g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE,
                1, G_TYPE_BOOLEAN);
}

/* Public methods */

HdvbReceiver *
hdvb_receiver_new(GUdevDevice *udev)
{
    return HDVB_RECEIVER(g_object_new(HDVB_TYPE_RECEIVER, "udev", udev, NULL));
}

int
hdvb_receiver_open_frontend(HdvbReceiver *rcv)
{
    g_return_val_if_fail(HDVB_IS_RECEIVER(rcv), -1);
    return HDVB_RECEIVER_GET_CLASS(rcv)->open_frontend(rcv);
}

void
hdvb_receiver_close_frontend_full(HdvbReceiver *rcv, gboolean emit_detune)
{
    g_return_if_fail(HDVB_IS_RECEIVER(rcv));
    HDVB_RECEIVER_GET_CLASS(rcv)->close_frontend_full(rcv, emit_detune);
}

int
hdvb_receiver_open_dvr(HdvbReceiver *rcv)
{
    g_return_val_if_fail(HDVB_IS_RECEIVER(rcv), -1);
    return HDVB_RECEIVER_GET_CLASS(rcv)->open_dvr(rcv);
}

void
hdvb_receiver_close_dvr(HdvbReceiver *rcv)
{
    g_return_if_fail(HDVB_IS_RECEIVER(rcv));
    HDVB_RECEIVER_GET_CLASS(rcv)->close_dvr(rcv);
}

int
hdvb_receiver_tune(HdvbReceiver *rcv,
        const HdvbTuningData *td, const char *tuning_str,
        guint timeout)
{
    g_return_val_if_fail(HDVB_IS_RECEIVER(rcv), -1);
    return HDVB_RECEIVER_GET_CLASS(rcv)->tune(rcv, td, tuning_str, timeout);
}

int
hdvb_receiver_request_ts_filter(HdvbReceiver *rcv,
        int pid, dmx_pes_type_t pes_type)
{
    g_return_val_if_fail(HDVB_IS_RECEIVER(rcv), -1);
    return HDVB_RECEIVER_GET_CLASS(rcv)->request_ts_filter(rcv, pid, pes_type);
}

void
hdvb_receiver_unref_ts_filter(HdvbReceiver *rcv, int pid)
{
    g_return_if_fail(HDVB_IS_RECEIVER(rcv));
    HDVB_RECEIVER_GET_CLASS(rcv)->unref_ts_filter(rcv, pid);
}

void
hdvb_receiver_poll_for_lock(HdvbReceiver *rcv, guint timeout)
{
    g_return_if_fail(HDVB_IS_RECEIVER(rcv));
    HDVB_RECEIVER_GET_CLASS(rcv)->poll_for_lock(rcv, timeout);
}

int
hdvb_receiver_set_section_filter(HdvbReceiver *rcv,
        const struct dmx_sct_filter_params *params)
{
    g_return_val_if_fail(HDVB_IS_RECEIVER(rcv), -1);
    return HDVB_RECEIVER_GET_CLASS(rcv)->set_section_filter(rcv, params);
}

void
hdvb_receiver_reserve(HdvbReceiver *rcv)
{
    g_return_if_fail(HDVB_IS_RECEIVER(rcv));
    HDVB_RECEIVER_GET_CLASS(rcv)->reserve(rcv);
}

void
hdvb_receiver_event_status(HdvbReceiver *rcv, gboolean running)
{
    g_return_if_fail(HDVB_IS_RECEIVER(rcv));
    HDVB_RECEIVER_GET_CLASS(rcv)->event_status(rcv, running);
}

HdvbStreamSrcPtr
hdvb_receiver_get_stream_src(HdvbReceiver *rcv)
{
    g_return_val_if_fail(HDVB_IS_RECEIVER(rcv), NULL);
    return HDVB_RECEIVER_GET_CLASS(rcv)->get_stream_src(rcv);
}

void
hdvb_receiver_set_stream_src_factory(HdvbReceiver *rcv,
        HdvbStreamSrcFactory factory, HdvbStreamSrcDeleter deleter,
        gpointer handle)
{
    g_return_if_fail(HDVB_IS_RECEIVER(rcv));
    HDVB_RECEIVER_GET_CLASS(rcv)->set_stream_src_factory(rcv,
            factory, deleter, handle);
}

fe_type_t
hdvb_receiver_get_fe_type(HdvbReceiver *rcv)
{
    struct dvb_frontend_info info;
    int fd = hdvb_receiver_open_frontend(rcv);

    if (fd == -1)
        return (fe_type_t) -1;
    inline_lock(rcv);
    fd = ioctl(fd, FE_GET_INFO, &info);
    inline_unlock(rcv);
    return (fd == - 1) ? (fe_type_t) -1 : info.type;
}

void
hdvb_receiver_lock(HdvbReceiver *rcv)
{
    inline_lock(rcv);
}

void
hdvb_receiver_unlock(HdvbReceiver *rcv)
{
    inline_unlock(rcv);
}

gboolean
hdvb_receiver_is_tuned_to(HdvbReceiver *rcv, const gchar *tuning_str)
{
    return rcv->tuning_str &&
            hdvb_tuning_strs_are_similar(rcv->tuning_str, tuning_str);
}

gboolean
hdvb_receiver_read_tuning_details(HdvbReceiver *rcv, HdvbTuningData *td)
{
    g_return_val_if_fail(HDVB_IS_RECEIVER(rcv), FALSE);
    return HDVB_RECEIVER_GET_CLASS(rcv)->read_tuning_details(rcv, td);
}


/* udev/factory */

static HdvbReceiverFactory hdvb_receiver_factory;
static gpointer hdvb_receiver_factory_handle;
GList *hdvb_receiver_receivers = NULL;

static gboolean
udevs_are_equivalent(GUdevDevice *d1, GUdevDevice *d2)
{
    return !strcmp(g_udev_device_get_device_file(d1),
            g_udev_device_get_device_file(d2));
}

/* device type srings seem to be broken so use device node name to check
 * whether we've got a frontend */
static gboolean
udev_is_dvb_frontend(GUdevDevice *udev)
{
    const char *devname = g_udev_device_get_device_file(udev);

    return strstr(devname, "adapter") && strstr(devname, "frontend");
}

static void
hdvb_receiver_create_from_udev(GUdevDevice *udev)
{
    const char *devname = g_udev_device_get_device_file(udev);
    GList *link;
    HdvbReceiver *rcv;

    if (!udev_is_dvb_frontend(udev))
        return;
    /* Check we don't already have this device */
    for (link = hdvb_receiver_receivers; link; link = g_list_next(link))
    {
        if (udevs_are_equivalent(HDVB_RECEIVER(link->data)->udev, udev))
        {
            g_warning(_("%s already registered"), devname);
            return;
        }
    }
    rcv = hdvb_receiver_factory ?
            hdvb_receiver_factory(udev, hdvb_receiver_factory_handle) :
            hdvb_receiver_new(udev);
    if (rcv)
        hdvb_receiver_receivers = g_list_append(hdvb_receiver_receivers, rcv);
}

static void
hdvb_receiver_remove_udev(GUdevDevice *udev)
{
    GList *link;

    for (link = hdvb_receiver_receivers; link; link = g_list_next(link))
    {
        HdvbReceiver *rcv = link->data;

        if (udevs_are_equivalent(rcv->udev, udev))
        {
            hdvb_receiver_receivers =
                    g_list_delete_link(hdvb_receiver_receivers, link);
            g_signal_emit(rcv,
                    hdvb_receiver_signals[HDVB_RECEIVER_SIGNAL_REMOVED], 0);
            g_object_unref(rcv);
            return;
        }
    }
    g_warning(_("Removal event for unknown device %s"),
            g_udev_device_get_device_file(udev));
}

static void
udev_event_cb(GUdevClient *client, gchar *action, GUdevDevice *udev,
        gpointer handle)
{
    if (!udev_is_dvb_frontend(udev))
        return;
    if (!strcmp(action, "add"))
        hdvb_receiver_create_from_udev(udev);
    else if (!strcmp(action, "remove"))
        hdvb_receiver_remove_udev(udev);
}

void
hdvb_receiver_udev_init(HdvbReceiverFactory factory, gpointer handle)
{
    static char const *subsys[] = {"dvb" , NULL };
    GUdevClient *udev_client = g_udev_client_new(subsys);
    GList *devices = g_udev_client_query_by_subsystem(udev_client,
            "dvb");
    GList *link;

    hdvb_receiver_factory = factory;
    hdvb_receiver_factory_handle = handle;

    for (link = devices; link; link = g_list_next(link))
    {
        hdvb_receiver_create_from_udev(link->data);
        g_object_unref(link->data);
    }
    if (devices)
        g_list_free(devices);
    g_signal_connect(udev_client, "uevent", G_CALLBACK(udev_event_cb), NULL);
}

HdvbReceiver *
hdvb_receiver_get_by_numbers(int adapter, int frontend)
{
    GList *link;

    for (link = hdvb_receiver_receivers; link; link = g_list_next(link))
    {
        HdvbReceiver *rcv = HDVB_RECEIVER(link->data);

        if (rcv->adapter == adapter && rcv->frontend == frontend)
            return rcv;
    }
    g_warning(_("No receiver registered for adapter%d/frontend%d"),
            adapter, frontend);
    return NULL;
}

GList *
hdvb_receiver_list_receivers(void)
{
    return hdvb_receiver_receivers;
}
